/*-
 * Copyright (c) 2014-2019 MongoDB, Inc.
 * Copyright (c) 2008-2014 WiredTiger, Inc.
 *	All rights reserved.
 *
 * See the file LICENSE for redistribution information.
 */

#include "wt_internal.h"

/*
 * __nsnap_destroy --
 *     Destroy a named snapshot structure.
 */
static void
__nsnap_destroy(WT_SESSION_IMPL *session, WT_NAMED_SNAPSHOT *nsnap)
{
    __wt_free(session, nsnap->name);
    __wt_free(session, nsnap->snapshot);
    __wt_free(session, nsnap);
}

/*
 * __nsnap_drop_one --
 *     Drop a single named snapshot. The named snapshot lock must be held write locked.
 */
static int
__nsnap_drop_one(WT_SESSION_IMPL *session, WT_CONFIG_ITEM *name)
{
    WT_NAMED_SNAPSHOT *found;
    WT_TXN_GLOBAL *txn_global;

    txn_global = &S2C(session)->txn_global;

    TAILQ_FOREACH (found, &txn_global->nsnaph, q)
        if (WT_STRING_MATCH(found->name, name->str, name->len))
            break;

    if (found == NULL)
        return (WT_NOTFOUND);

    /* Bump the global ID if we are removing the first entry */
    if (found == TAILQ_FIRST(&txn_global->nsnaph)) {
        WT_ASSERT(session, !__wt_txn_visible_all(session, txn_global->nsnap_oldest_id, WT_TS_NONE));
        txn_global->nsnap_oldest_id =
          (TAILQ_NEXT(found, q) != NULL) ? TAILQ_NEXT(found, q)->pinned_id : WT_TXN_NONE;
        WT_DIAGNOSTIC_YIELD;
        WT_ASSERT(session, txn_global->nsnap_oldest_id == WT_TXN_NONE ||
            !__wt_txn_visible_all(session, txn_global->nsnap_oldest_id, WT_TS_NONE));
    }
    TAILQ_REMOVE(&txn_global->nsnaph, found, q);
    __nsnap_destroy(session, found);
    WT_STAT_CONN_INCR(session, txn_snapshots_dropped);

    return (0);
}

/*
 * __nsnap_drop_to --
 *     Drop named snapshots, if the name is NULL all snapshots will be dropped. The named snapshot
 *     lock must be held write locked.
 */
static int
__nsnap_drop_to(WT_SESSION_IMPL *session, WT_CONFIG_ITEM *name, bool inclusive)
{
    WT_NAMED_SNAPSHOT *last, *nsnap, *prev;
    WT_TXN_GLOBAL *txn_global;
    uint64_t new_nsnap_oldest;

    last = nsnap = prev = NULL;
    txn_global = &S2C(session)->txn_global;

    if (TAILQ_EMPTY(&txn_global->nsnaph)) {
        if (name == NULL)
            return (0);
        /*
         * Dropping specific snapshots when there aren't any it's an error.
         */
        WT_RET_MSG(
          session, EINVAL, "Named snapshot '%.*s' for drop not found", (int)name->len, name->str);
    }

    /*
     * The new ID will be none if we are removing all named snapshots which is the default behavior
     * of this loop.
     */
    new_nsnap_oldest = WT_TXN_NONE;
    if (name != NULL) {
        TAILQ_FOREACH (last, &txn_global->nsnaph, q) {
            if (WT_STRING_MATCH(last->name, name->str, name->len))
                break;
            prev = last;
        }
        if (last == NULL)
            WT_RET_MSG(session, EINVAL, "Named snapshot '%.*s' for drop not found", (int)name->len,
              name->str);

        if (!inclusive) {
            /* We are done if a drop before points to the head */
            if (prev == 0)
                return (0);
            last = prev;
        }

        if (TAILQ_NEXT(last, q) != NULL)
            new_nsnap_oldest = TAILQ_NEXT(last, q)->pinned_id;
    }

    do {
        nsnap = TAILQ_FIRST(&txn_global->nsnaph);
        WT_ASSERT(session, nsnap != NULL);
        TAILQ_REMOVE(&txn_global->nsnaph, nsnap, q);
        __nsnap_destroy(session, nsnap);
        WT_STAT_CONN_INCR(session, txn_snapshots_dropped);
        /* Last will be NULL in the all case so it will never match */
    } while (nsnap != last && !TAILQ_EMPTY(&txn_global->nsnaph));

    /* Now that the queue of named snapshots is updated, update the ID */
    WT_ASSERT(session, !__wt_txn_visible_all(session, txn_global->nsnap_oldest_id, WT_TS_NONE) &&
        (new_nsnap_oldest == WT_TXN_NONE ||
                WT_TXNID_LE(txn_global->nsnap_oldest_id, new_nsnap_oldest)));
    txn_global->nsnap_oldest_id = new_nsnap_oldest;
    WT_DIAGNOSTIC_YIELD;
    WT_ASSERT(session, new_nsnap_oldest == WT_TXN_NONE ||
        !__wt_txn_visible_all(session, new_nsnap_oldest, WT_TS_NONE));

    return (0);
}

/*
 * __wt_txn_named_snapshot_begin --
 *     Begin an named in-memory snapshot.
 */
int
__wt_txn_named_snapshot_begin(WT_SESSION_IMPL *session, const char *cfg[])
{
    WT_CONFIG_ITEM cval;
    WT_DECL_RET;
    WT_NAMED_SNAPSHOT *nsnap, *nsnap_new;
    WT_TXN *txn;
    WT_TXN_GLOBAL *txn_global;
    const char *txn_cfg[] = {
      WT_CONFIG_BASE(session, WT_SESSION_begin_transaction), "isolation=snapshot", NULL};
    bool include_updates, started_txn;

    started_txn = false;
    nsnap_new = NULL;
    txn_global = &S2C(session)->txn_global;
    txn = &session->txn;

    WT_RET(__wt_config_gets_def(session, cfg, "include_updates", 0, &cval));
    include_updates = cval.val != 0;

    WT_RET(__wt_config_gets_def(session, cfg, "name", 0, &cval));
    WT_ASSERT(session, cval.len != 0);

    if (!F_ISSET(txn, WT_TXN_RUNNING)) {
        if (include_updates)
            WT_RET_MSG(session, EINVAL,
              "A transaction must be "
              "running to include updates in a named snapshot");

        WT_RET(__wt_txn_begin(session, txn_cfg));
        started_txn = true;
    }
    if (!include_updates)
        F_SET(txn, WT_TXN_READONLY);

    /* Save a copy of the transaction's snapshot. */
    WT_ERR(__wt_calloc_one(session, &nsnap_new));
    nsnap = nsnap_new;
    WT_ERR(__wt_strndup(session, cval.str, cval.len, &nsnap->name));

    /*
     * To include updates from a writing transaction, make sure a transaction ID has been allocated.
     */
    if (include_updates) {
        WT_ERR(__wt_txn_id_check(session));
        WT_ASSERT(session, txn->id != WT_TXN_NONE);
        nsnap->id = txn->id;
    } else
        nsnap->id = WT_TXN_NONE;
    nsnap->pinned_id = WT_SESSION_TXN_STATE(session)->pinned_id;
    nsnap->snap_min = txn->snap_min;
    nsnap->snap_max = txn->snap_max;
    if (txn->snapshot_count > 0) {
        WT_ERR(__wt_calloc_def(session, txn->snapshot_count, &nsnap->snapshot));
        memcpy(nsnap->snapshot, txn->snapshot, txn->snapshot_count * sizeof(*nsnap->snapshot));
    }
    nsnap->snapshot_count = txn->snapshot_count;

    /* Update the list. */

    /*
     * The semantic is that a new snapshot with the same name as an existing snapshot will replace
     * the old one.
     */
    WT_ERR_NOTFOUND_OK(__nsnap_drop_one(session, &cval));

    if (TAILQ_EMPTY(&txn_global->nsnaph)) {
        WT_ASSERT(session, txn_global->nsnap_oldest_id == WT_TXN_NONE &&
            !__wt_txn_visible_all(session, nsnap_new->pinned_id, WT_TS_NONE));
        __wt_readlock(session, &txn_global->rwlock);
        txn_global->nsnap_oldest_id = nsnap_new->pinned_id;
        __wt_readunlock(session, &txn_global->rwlock);
    }
    TAILQ_INSERT_TAIL(&txn_global->nsnaph, nsnap_new, q);
    WT_STAT_CONN_INCR(session, txn_snapshots_created);
    nsnap_new = NULL;

err:
    if (started_txn) {
#ifdef HAVE_DIAGNOSTIC
        uint64_t pinned_id = WT_SESSION_TXN_STATE(session)->pinned_id;
#endif
        WT_TRET(__wt_txn_rollback(session, NULL));
        WT_DIAGNOSTIC_YIELD;
        WT_ASSERT(session, !__wt_txn_visible_all(session, pinned_id, WT_TS_NONE));
    }

    if (nsnap_new != NULL)
        __nsnap_destroy(session, nsnap_new);

    return (ret);
}

/*
 * __wt_txn_named_snapshot_drop --
 *     Drop named snapshots
 */
int
__wt_txn_named_snapshot_drop(WT_SESSION_IMPL *session, const char *cfg[])
{
    WT_CONFIG objectconf;
    WT_CONFIG_ITEM all_config, before_config, k, names_config, to_config, v;
    WT_DECL_RET;

    WT_RET(__wt_config_gets_def(session, cfg, "drop.all", 0, &all_config));
    WT_RET(__wt_config_gets_def(session, cfg, "drop.names", 0, &names_config));
    WT_RET(__wt_config_gets_def(session, cfg, "drop.to", 0, &to_config));
    WT_RET(__wt_config_gets_def(session, cfg, "drop.before", 0, &before_config));

    if (all_config.val != 0)
        WT_RET(__nsnap_drop_to(session, NULL, true));
    else if (before_config.len != 0)
        WT_RET(__nsnap_drop_to(session, &before_config, false));
    else if (to_config.len != 0)
        WT_RET(__nsnap_drop_to(session, &to_config, true));

    /* We are done if there are no named drops */

    if (names_config.len != 0) {
        __wt_config_subinit(session, &objectconf, &names_config);
        while ((ret = __wt_config_next(&objectconf, &k, &v)) == 0) {
            ret = __nsnap_drop_one(session, &k);
            if (ret != 0)
                WT_RET_MSG(
                  session, EINVAL, "Named snapshot '%.*s' for drop not found", (int)k.len, k.str);
        }
        if (ret == WT_NOTFOUND)
            ret = 0;
    }

    return (ret);
}

/*
 * __wt_txn_named_snapshot_get --
 *     Lookup a named snapshot for a transaction.
 */
int
__wt_txn_named_snapshot_get(WT_SESSION_IMPL *session, WT_CONFIG_ITEM *nameval)
{
    WT_NAMED_SNAPSHOT *nsnap;
    WT_TXN *txn;
    WT_TXN_GLOBAL *txn_global;
    WT_TXN_STATE *txn_state;

    txn = &session->txn;
    txn_global = &S2C(session)->txn_global;
    txn_state = WT_SESSION_TXN_STATE(session);

    txn->isolation = WT_ISO_SNAPSHOT;
    if (session->ncursors > 0)
        WT_RET(__wt_session_copy_values(session));

    __wt_readlock(session, &txn_global->nsnap_rwlock);
    TAILQ_FOREACH (nsnap, &txn_global->nsnaph, q)
        if (WT_STRING_MATCH(nsnap->name, nameval->str, nameval->len)) {
            /*
             * Acquire the scan lock so the oldest ID can't move forward without seeing our pinned
             * ID.
             */
            __wt_readlock(session, &txn_global->rwlock);
            txn_state->pinned_id = nsnap->pinned_id;
            __wt_readunlock(session, &txn_global->rwlock);

            WT_ASSERT(session, !__wt_txn_visible_all(session, txn_state->pinned_id, WT_TS_NONE) &&
                txn_global->nsnap_oldest_id != WT_TXN_NONE &&
                WT_TXNID_LE(txn_global->nsnap_oldest_id, txn_state->pinned_id));
            txn->snap_min = nsnap->snap_min;
            txn->snap_max = nsnap->snap_max;
            if ((txn->snapshot_count = nsnap->snapshot_count) != 0)
                memcpy(
                  txn->snapshot, nsnap->snapshot, nsnap->snapshot_count * sizeof(*nsnap->snapshot));
            if (nsnap->id != WT_TXN_NONE) {
                WT_ASSERT(session, txn->id == WT_TXN_NONE);
                txn->id = nsnap->id;
                F_SET(txn, WT_TXN_READONLY);
            }
            F_SET(txn, WT_TXN_HAS_SNAPSHOT);
            break;
        }
    __wt_readunlock(session, &txn_global->nsnap_rwlock);

    if (nsnap == NULL)
        WT_RET_MSG(
          session, EINVAL, "Named snapshot '%.*s' not found", (int)nameval->len, nameval->str);

    /* Flag that this transaction is opened on a named snapshot */
    F_SET(txn, WT_TXN_NAMED_SNAPSHOT);

    return (0);
}

/*
 * __wt_txn_named_snapshot_config --
 *     Check the configuration for a named snapshot
 */
int
__wt_txn_named_snapshot_config(
  WT_SESSION_IMPL *session, const char *cfg[], bool *has_create, bool *has_drops)
{
    WT_CONFIG_ITEM all_config, before_config, names_config, to_config;
    WT_CONFIG_ITEM cval;
    WT_TXN *txn;

    txn = &session->txn;
    *has_create = *has_drops = false;

    /* Verify that the name is legal. */
    WT_RET(__wt_config_gets_def(session, cfg, "name", 0, &cval));
    if (cval.len != 0) {
        if (WT_STRING_MATCH("all", cval.str, cval.len))
            WT_RET_MSG(session, EINVAL, "Can't create snapshot with reserved \"all\" name");

        WT_RET(__wt_name_check(session, cval.str, cval.len));

        if (F_ISSET(txn, WT_TXN_RUNNING) && txn->isolation != WT_ISO_SNAPSHOT)
            WT_RET_MSG(session, EINVAL,
              "Can't create a named snapshot from a running "
              "transaction that isn't snapshot isolation");
        else if (F_ISSET(txn, WT_TXN_RUNNING) && txn->mod_count != 0)
            WT_RET_MSG(session, EINVAL,
              "Can't create a named snapshot from a running "
              "transaction that has made updates");
        *has_create = true;
    }

    /* Verify that the drop configuration is sane. */
    WT_RET(__wt_config_gets_def(session, cfg, "drop.all", 0, &all_config));
    WT_RET(__wt_config_gets_def(session, cfg, "drop.names", 0, &names_config));
    WT_RET(__wt_config_gets_def(session, cfg, "drop.to", 0, &to_config));
    WT_RET(__wt_config_gets_def(session, cfg, "drop.before", 0, &before_config));

    /* Avoid more work if no drops are configured. */
    if (all_config.val != 0 || names_config.len != 0 || before_config.len != 0 ||
      to_config.len != 0) {
        if (before_config.len != 0 && to_config.len != 0)
            WT_RET_MSG(session, EINVAL,
              "Illegal configuration; named snapshot drop can't "
              "specify both before and to options");
        if (all_config.val != 0 &&
          (names_config.len != 0 || to_config.len != 0 || before_config.len != 0))
            WT_RET_MSG(session, EINVAL,
              "Illegal configuration; named snapshot drop can't "
              "specify all and any other options");
        *has_drops = true;
    }

    if (!*has_create && !*has_drops)
        WT_RET_MSG(session, EINVAL,
          "WT_SESSION::snapshot API called without any drop or "
          "name option");

    return (0);
}

/*
 * __wt_txn_named_snapshot_destroy --
 *     Destroy all named snapshots on connection close
 */
void
__wt_txn_named_snapshot_destroy(WT_SESSION_IMPL *session)
{
    WT_NAMED_SNAPSHOT *nsnap;
    WT_TXN_GLOBAL *txn_global;

    txn_global = &S2C(session)->txn_global;
    txn_global->nsnap_oldest_id = WT_TXN_NONE;

    while ((nsnap = TAILQ_FIRST(&txn_global->nsnaph)) != NULL) {
        TAILQ_REMOVE(&txn_global->nsnaph, nsnap, q);
        __nsnap_destroy(session, nsnap);
    }
}
